<template>
	<div ref="listContainer" class="wind-virtual-list" @scroll="onScroll">
		<div ref="phantomContent" class="wind-virtual-list__phantom-content" :style="{height: `${phantomHeight}px`}"></div>
		<div ref="actualContent" class="wind-virtual-list__actual-content" :style="{
			transform: getTransform
		}">
			<div
				v-for="item in visiableData"
				:key="item.id"
				class="wind-virtual-list__item"
				:data-item="`item-${item.$fxIndex}`"
			>
				<slot :item="item" :fxIndex="item.$fxIndex"></slot>
			</div>
		</div>
	</div>
</template>
<script lang="ts">
/**
 * VirtualList by shang 2022/05/17
 * @desc VirtualList 虚拟list
 * @param {Array} data list数据
 * @param {Number} limit 渲染条数
 * @param {Number} bufferSize 缓冲条数
 * @param {Number} estimatedRowHeight 估算行高
 */
import type { PropType } from 'vue'
import { defineComponent, ref, watch, computed, nextTick } from 'vue'
interface PositionItem {
	index: number
	height: number
	top: number
	bottom: number
	dValue: number
}
export default defineComponent({
	name: 'w-virtual-list',
	props: {
		data: {
			type: Array as PropType<{$fxIndex: number, id: string}[]>,
			default: () => {
				return []
			}
		},
		limit: {
			type: Number,
			default: 30
		},
		bufferSize: {
			type: Number,
			default: 5
		},
		estimatedRowHeight: {
			type: Number,
			default: 100
		},
		col: {
			type: Number,
			default: 3
		}
	},
	setup (props) {
		const startIndex = ref(0)
		const endIndex = ref(0)
		const scrollTop = ref(0)
		const originStartIdx = ref(0)
		const cachedPositions = ref<PositionItem[]>([])
		const phantomHeight = ref(0)
		const listContainer = ref<HTMLElement|null>(null)
		const actualContent = ref<HTMLElement|null>(null)
		const visiableData = computed(() => {
			return props.data.slice(startIndex.value, endIndex.value)
		})
		const getTransform = computed(() => {
			const translateY = startIndex.value >= 1 ? cachedPositions.value[startIndex.value - 1].bottom : 0
			return `translate3d(0,${translateY}px,0)`
		})
		const total = computed(() => {
			return props.data.length
		})
		const initTableData = (tableData:{$fxIndex: number}[]) => {
			tableData.forEach((item, index) => {
				item.$fxIndex = index
			})
			return tableData
		}
		const updateVisibleData = () => {
			endIndex.value = Math.min(0 + props.limit + props.bufferSize, props.data.length)
		}
		const initCachedPositions = () => {
			cachedPositions.value = []
			for (let i = 0; i < total.value; ++i) {
				cachedPositions.value[i] = {
					index: i,
					height: props.estimatedRowHeight,
					top: i * props.estimatedRowHeight,
					bottom: (i + 1) * props.estimatedRowHeight,
					dValue: 0
				}
			}
		}
		const resetAllVirtualParam = () => {
			originStartIdx.value = 0
			startIndex.value = 0
			endIndex.value = Math.min(
				originStartIdx.value + props.limit + props.bufferSize,
				total.value
			)
			if (listContainer.value) {
				listContainer.value.scrollTop = 0
			}
			initCachedPositions()
			phantomHeight.value = props.estimatedRowHeight * total.value
			scrollTop.value = 0
		}
		const updateCachedPositions = () => {
			if (!cachedPositions.value.length) {
				return false
			}
			const nodes = actualContent.value?.childNodes || []
			const start = nodes[0]
			nodes.forEach((node:Node) => {
				const rect = (node as Element).getBoundingClientRect()
				const { height } = rect
				const index = Number((node as HTMLDivElement).dataset?.item?.split('-')[1])
				const oldHeight = cachedPositions.value[index].height
				const dValue = oldHeight - height
				if (dValue) {
					cachedPositions.value[index].bottom -= dValue
					cachedPositions.value[index].height = height
					cachedPositions.value[index].dValue = dValue
				}
			})
			let startIdx = 0
			if (start) {
				startIdx = Number((start as HTMLDivElement).dataset?.item?.split('-')[1])
			}
			const cachedPositionsLen = cachedPositions.value.length
			let cumulativeDiffHeight = cachedPositions.value[startIdx].dValue
			cachedPositions.value[startIdx].dValue = 0
			for (let i = startIdx + 1; i < cachedPositionsLen; ++i) {
				const item = cachedPositions.value[i]
				cachedPositions.value[i].top = cachedPositions.value[i - 1].bottom
				cachedPositions.value[i].bottom = cachedPositions.value[i].bottom - cumulativeDiffHeight
				if (item.dValue !== 0) {
					cumulativeDiffHeight += item.dValue
					item.dValue = 0
				}
			}
			const height = cachedPositions.value[cachedPositionsLen - 1].bottom
			phantomHeight.value = height
		}
		const onScroll = (e:Event) => {
			const _scrollTop = (e.target as HTMLElement).scrollTop
			const currentStartIndex = getStartIndex(_scrollTop)
			if (currentStartIndex !== originStartIdx.value) {
				originStartIdx.value = currentStartIndex
				startIndex.value = Math.max(originStartIdx.value - props.bufferSize, 0)
				endIndex.value = Math.min(
					originStartIdx.value + props.limit + props.bufferSize,
					total.value
				)
				scrollTop.value = _scrollTop
				nextTick(() => {
					updateCachedPositions()
				})
			}
		}
		const getStartIndex = (scrollTop = 0):number => {
			let idx = binarySearch(cachedPositions.value, scrollTop, (currentValue:PositionItem, targetValue:number) => {
				const currentCompareValue = currentValue.bottom
				if (currentCompareValue === targetValue) {
					return 'eq'
				}
				if (currentCompareValue < targetValue) {
					return 'lt'
				}
				return 'gt'
			})
			const targetItem = cachedPositions.value[idx]
			if (targetItem.bottom < scrollTop) {
				idx += 1
			}
			return idx
		}
		const binarySearch = (list:PositionItem[], value:number, compareFunc:Function):number => {
			let start = 0
			let end = list.length - 1
			let tempIndex = 0

			while (start <= end) {
				tempIndex = Math.floor((start + end) / 2)
				const midValue = list[tempIndex]
				const compareRes = compareFunc(midValue, value)
				if (compareRes === 'eq') {
					return tempIndex
				}
				if (compareRes === 'lt') {
					start = tempIndex + 1
				} else if (compareRes === 'gt') {
					end = tempIndex - 1
				}
			}
			return tempIndex
		}
		watch(() => props.data, (value) => {
			initTableData(value as {$fxIndex: number}[])
			phantomHeight.value = Math.ceil((value.length / props.col)) * props.estimatedRowHeight
			nextTick(() => {
				updateVisibleData()
				initCachedPositions()
				nextTick(() => {
					updateCachedPositions()
				})
			})
		}, {
			immediate: true
		})
		return {
			phantomHeight,
			listContainer,
			onScroll,
			visiableData,
			getTransform,
			resetAllVirtualParam,
			updateCachedPositions
		}
	}
})
</script>
<style lang="scss" scoped>
@import "$assets/stylus/varsty";
.wind-virtual-list {
    position: relative;
    display: flex;
    overflow: auto;
    overflow-y: auto;
    padding-top: 10px;
    padding-left: 10px;
    height: 100%;
    flex: 1;
    box-sizing: border-box;
    &__phantom-content {
        position: relative;
    }
    &__actual-content {
        position: absolute;
        top: 0;
        display: flex;
        overflow: auto;
        width: 100%;
        background-color: #eff3fb;
        flex: 1;
        flex-flow: row wrap;
        align-content: flex-start;
        box-sizing: border-box;
    }
    &__item {
        position: relative;
        overflow: hidden;
        margin-right: 10px;
        margin-bottom: 10px;
        width: 150px;
        border-radius: 5px;
        background-color: $fxWhite;
    }
}
</style>
